package org.hawk.gwt.ppc.scanner.utils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.utils.Name;

/**
 * Represents java method declaration. Allows to get all
 * the method attributes such as name, return type and arguments.
 * TODO: this class and {@link JavaConstructor} has duplicated code for parameters and generic parameters.
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public class JavaMethod extends JavaScope implements JavaNameResolver {
	private String name;
	private String returnType;
	private String[] parametersTypes;
	private boolean varargs;
	private Map<String, JavaTypeVariable> params;
	
	JavaMethod(String name, JavaScope parent, int start, List<JavaVariable> vars, String returnType, boolean varargs){
		super(parent,start,vars);
		this.name = name;
		this.returnType = returnType;
		this.parametersTypes = extractParametersTypes(vars);
		this.varargs = varargs;
	}

	protected JavaTypeScope parseGenericType(String type) {
		String pureType = Name.getPureClassName(type);
		JavaTypeVariable var = resolveTypeParameter(pureType);
		if (var != null) {
			JavaTypeScope result = var;
			String name = type;
			while (name.endsWith("[]")) {
				result = new JavaArrayGenericTypeScope(result, this);
				name = name.substring(0, name.length() - 2);
			}
			return result;
		}
		JavaTypeScope raw = getParent().resolveTypeDescriptor(type).getCompilationUnit();
		String params = Name.getTypeParameters(type);
		if (params.isEmpty()) {
			return raw;
		}
		return new JavaParameterizedGenericTypeScope(raw, this, params);
	}

	@Override
	void setModifiers(List<JavaModifier> modifiers) {
		if (getParent().isInterface()) {
			if (!modifiers.contains(JavaModifier.ABSTRACT)) {
				modifiers.add(JavaModifier.ABSTRACT);
			}
			if (!modifiers.contains(JavaModifier.PUBLIC)) {
				modifiers.add(JavaModifier.PUBLIC);
			}
		}
		super.setModifiers(modifiers);
	}
	
	void addTypeParameter(String name, List<String> bounds) throws IOException, ParseException {
		if (params == null) {
			params = new HashMap<String, JavaTypeVariable>();
		}
		params.put(name, new JavaTypeVariable(name, getParent(), this, bounds));
	}

	@Override
	public JavaTypeScope getParent() {
		return (JavaTypeScope)super.getParent();
	}

	private String[] extractParametersTypes(List<JavaVariable> vars) {
		String[] result = new String[vars.size()];
		Iterator<JavaVariable> iterator = vars.iterator();
		for(int i = 0; i < vars.size(); i++) {
			JavaVariable param = iterator.next();
			param.setScope(this);
			result[i] = param.getRawTypeName();
		}
		return result;
	}
	
	/**
	 * @return name of this method
	 */
	public String getName() {
		return name;
	}
	
	/**
	 * @return array of parameters types of this method
	 */
	public String[] getParametersTypes() {
		String[] result = new String[parametersTypes.length];
		for(int i=0; i < parametersTypes.length; i++) {
			result[i] = resolveType(parametersTypes[i]);
		}
		return result;
	}
	/**
	 * @return type parameters of this method
	 */
	public Collection<JavaTypeVariable> getTypeParameters() {
		if (params == null) {
			return Collections.emptyList();
		}
		return Collections.unmodifiableCollection(params.values());
	}
	/**
	 * @return generic type parameters of this method
	 */
	public List<JavaTypeScope> getGenericParameterTypes() {
		String[] rawParams = getRawParams();
		List<JavaTypeScope> result = new ArrayList<JavaTypeScope>(rawParams.length);
		boolean notGeneric = true;
		for(int i = 0; i < rawParams.length; i++) {
			JavaTypeScope param = parseGenericType(rawParams[i]);
			if (param instanceof JavaGenericTypeScope) {
				notGeneric = false;
			}
			result.add(param);
		}
		return notGeneric ? null : Collections.unmodifiableList(result);
	}

	public JavaTypeScope getGenericReturnType() {
		return parseGenericType(returnType);
	}
	
	protected String[] getRawParams() {
		return parametersTypes;
	}
	
	/**
	 * @return return type of this method
	 */
	public String getReturnType(){
		return Name.getRawName(resolveType(returnType));
	}
	
	public boolean isVarArgs() {
		return varargs;
	}

	@Override
	public String resolveType(String typeSimpleName) {
		String pureName = Name.getPureClassName(typeSimpleName);
		JavaTypeVariable var = resolveTypeParameter(pureName);
		if (var != null) {
			return ( var.getBounds().isEmpty() ? Object.class.getName() : var.getBounds().get(0).getResolvedClassName() ) 
					+ typeSimpleName.substring(pureName.length());
		}
		return super.resolveType(typeSimpleName);
	}

	public JavaTypeVariable resolveTypeParameter(String name) {
		if (params != null && params.get(name) != null) {
			return params.get(name);
		}
		if (getParent() == null) {
			return null;
		}
		return getParent().resolveTypeParameter(name);
	}

	public JavaClassDescriptor resolveTypeDescriptor(String typeName) {
		return getParent().resolveTypeDescriptor(typeName);
	}

	boolean matches(String name, String...parametersTypes) {
		return this.name.equals(name) && paramsMatches(getParametersTypes(), parametersTypes);
	}

	private boolean paramsMatches(String[] expected,String[] search) {
		if (expected.length != search.length && !varargs) {
			return false;
		}
		final int lastIndex = expected.length-1;
		int i = 0;
		for(; i < search.length; i++) {
			String param = null;
			boolean isLast = false;
			if (i < lastIndex) {
				param = expected[i];
			} else {
				isLast = true;
				param = expected[lastIndex];
			}
			if (isLast && paramsMatches(param,search[i], false)) {
				// array as var args - valid result 
				// only if it is last param
				return i == (search.length - 1);
			}
			if (!paramsMatches(param,search[i], isLast)) {
				return false;
			}
		}
		return i >= lastIndex;
	}

	private boolean paramsMatches(String expected, String search,boolean last) {
		if (getParent() == null) {
			// may be helpful for testing purposes
			return expected.equals(search);
		}
		JavaClassDescriptor expectedType = getParent().resolveTypeDescriptor(expected);
		if (varargs && last) {
			if (!expectedType.isArray()) {
				return false;
			}
			expectedType = expectedType.getArrayElement();
		}
		return expectedType.getCompilationUnit().isAssignableFrom(
				getParent().resolveTypeDescriptor(search).getCompilationUnit());
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof JavaMethod) {
			// methods are equal if signatures are equal.
			return toString().equals(String.valueOf(obj));
		}
		return super.equals(obj);
	}
	/**
	 * @return method signature (name and parameters)
	 */
	public String getSignature() {
		StringBuilder result = new StringBuilder();
		result.append(getName()).append('(');
		boolean first = true;
		for(String param : getParametersTypes()){
			if(first)first=false;
			else result.append(',');
			result.append(Name.getRawName(param));
		}
		result.append(')');
		return result.toString();
	}

	@Override
	public String toString() {
		return getSignature();
	}
}